Дисклеймер: мы не описываем подробно «магическую суть» и историю возникновения инструмента (об этом уже рассказано немало). Надеемся, что тем, кто уже знаком с GraphQL, окажется полезен наш практический опыт.
Недавно мы приняли участие в создании интернет-каталога светотехнического оборудования. Для создания рекламных страниц администраторы сайта могли воспользоваться конструктором: выбрать нужные блоки (например, баннеры или списки), заполнить их данными, определить порядок отображения и другие настройки. При этом приложение рендерило компоненты, заранее сверстанные для каждого типа блоков.
В каждой категории интернет-каталога выводились карточки различных товарных групп, и при наведении на карточку отображался список свойств для вложенных товаров.
Нам нужно было выводить свойства товаров в древовидной структуре и обеспечить достаточно высокую скорость обработки запросов.
Мы выбрали следующий порядок работы с запросами:
- Запросить все товарные группы для определенной категории (как правило, около 50 групп).
- Запросить список товаров для каждой группы.
- Запросить список свойств для каждого товара.
Поскольку мы разрабатывали приложение на базе GraphQL, мы были готовы к тому, что часть данных будет иметь достаточно сложную вложенную структуру. Хотя для backend-разработки ветвление этой структуры было логичным, на фронте нужно было писать некоторую «лишнюю» логику, чтобы обработать данные и вывести их в компонент, согласно дизайну.
Из-за особенностей конструктора GraphQL мы приняли решение осуществлять сбор свойств и уникальных значений не на бэке, а на фронте, а затем рендерить их в определенном порядке. Однако, обработка запроса происходила слишком медленно – до 20 секунд, что нас, конечно, не устраивало.
По этой причине мы стали делить каждый запрос на мелкие подзапросы и загружать данные порционно. В результате приложение заметно выиграло в скорости – запросы занимали не более 2 секунд. Хотя количество запросов стало больше, нагрузка на систему снизилась, исчезла необходимость подгружать неиспользуемые данные.
Далее расскажем подробнее непосредственно о работе с GraphQL.
Особенности работы с GraphQL
Согласно требованиям к продукту, нам следовало использовать язык запросов GraphQL, разработанный Facebook. По этой причине мы не стали пускаться в бесконечные споры, что лучше, GraphQL или REST – вместо этого мы решили использовать нужную технологию наиболее эффективным способом, учитывая все ее сильные стороны.
Мы учитывали, что GraphQL был разработан для упрощения разработки и поддержки API, прежде всего, за счет наличия единственной конечной точки.
GET /news GET /posts POST /news POST /post
GraphQL имеет единственную конечную точку. Это означает, что для получения данных из двух разных ресурсов нам не нужно делать два отдельных запроса. GraphQL объединяет все запросы и мутации в одной конечной точке и делает её доступной для обращения, а также позволяет уйти от версионирования, свойственного REST API.
GraphQL обеспечивает возможность оптимизировать производительность и получать именно те данные, которые необходимы в данный момент, с помощью особого синтаксиса запросов: требуемые поля следует перечислить в запросе.
const FETCH_USER_DATA = gql` query FetchUserData { user { firstName lastName date } } `;
GraphQL использует строго типизированные сущности и схему типов, что, в свою очередь, удобно в связке с TypeScript и генерацией типов на фронте.
Многие из перечисленных особенностей, безусловно, можно реализовать и на REST API, однако, GraphQL предоставляет их «из коробки».
Для взаимодействия клиента с GraphQL мы выбрали наиболее популярное решение с хорошей документацией – библиотеку Apollo Client, которая позволяет получать, кэшировать и модифицировать данные приложения. Apollo Client дает возможность использовать хуки для запросов и мутаций и инструменты для удобного отслеживания состояния загрузки/ошибки.
Также на фронте мы использовали фреймворк NextJS, выбранный с учетом следующих факторов: пре-рендеринг (NextJS предоставляет очень простой механизм реализации статической генерации и SSR “из коробки”), поддержка всех существующих css-in-js решений, динамический роутинг, поддержка статических файлов (например, изображений) в React-компонентах.
Наконец, когда технологии выбраны, перейдем к разработке. На первый взгляд, все смотрится неплохо: современные библиотеки, хорошая документация, много различных кейсов использования. Каждая из технологий по отдельности призвана способствовать комфортной и быстрой разработке. Создавая бойлерплейт, без которого было не обойтись, и верстая UI-компоненты, мы постепенно приблизились к этапу эффективного взаимодействия наших библиотек. Здесь началось все самое интересное.
Заглянув поглубже в механизмы NextJS, мы видим, что он использует две формы пререндера: статическую генерацию и SSR. Обе эти стратегии реализуются с помощью специальных функций предзагрузки данных:
`getInitialProps` (SSR) – при первой загрузке запускается на сервере, запрашивает данные и затем передает их в компонент в качестве props.
function Component ({data}) { ... } Component.getInitialProps = async (ctx) => { const res = await fetch('https://...') const json = await res.json() return { data: json.data } }
`getStaticProps` (Static Generation) – запускается на build-этапе. NextJS производит пре-рендер страницы с использованием props, вернувшихся из данной функции.
export async function getStaticProps(context) { return { props: {}, // передает данные в компонент в качестве props } }
`getServerSideProps` (Server Side Rendering) – NextJS производит пре-рендер страницы при каждом запросе, используя данные, вернувшиеся из данной функции в качестве props.
export async function getServerSideProps(context) { return { props: {}, // передает данные в компонент в качестве props } }
Таким образом, все перечисленные функции объявляются вне компонента, получают некоторые данные и передают в компонент. Отсюда вытекает одна из проблем взаимодействия с Apollo Client. Библиотека предоставляет такие механизмы запросов, как хук `useQuery` и компонент `Query`, при этом ни один из предложенных способов невозможно использовать вне компонента. С учетом этого в нашем проекте мы решили использовать библиотеку next-with-apollo и в итоге остались довольны результатом.
Еще одна известная проблема Apollo Client, с которой нам также довелось столкнуться – это возможность появления бесконечного цикла запросов из хука `useQuery`. Суть проблемы заключается в том, что Apollo Client продолжает бесконечно отправлять запросы до тех пор, пока успешно не получит данные. Это может привести к ситуации, когда компонент “повиснет” в бесконечном запросе, если сервер по какой-либо причине не может вернуть данные.
В нашем случае одной из причин служило изменение схемы на бэке. Apollo самостоятельно генерирует типы, поэтому при написании запросов на фронте мы должны были следовать сгенерированным типам, чтобы избежать ошибок. Например, у нас был рабочий запрос, без каких-либо проблем с типами. Однако, при изменении схемы на бэке одновременно менялись типы, из-за чего рабочий запрос мог перестать функционировать. Учитывая это, оптимально сразу внедрять логирование и четкую систему обработки ошибок, чтобы сэкономить нервы и время команды.
Достаточно полезным, на наш взгляд, оказалось то, что в graphql-запросах можно точно указать, какие данные следует получать. При отправке большого количества запросов это позволяет избежать обработки излишних данных. В свою очередь, это может существенно повлиять на производительность, когда растет количество данных.
Стоит отметить, что одним из преимуществ GraphQL считается удобство разработки и поддержки API, но это свойство более значимо для backend-разработки и, по нашим наблюдениям, не оказывает значительного влияния на фронт. Из-за генерации типов при каждом изменении схемы на бэке мы переписывали все затронутые запросы. Бэку также приходилось дорабатывать схему, если нам на фронте нужно было получить что-то, что еще не было реализовано. При этом генерация типов при использовании TypeScript позволяла отлавливать многие ошибки еще на стадии написания кода.
Подводя итоги
По нашим наблюдениям, GraphQL достаточно широко востребован в различных типах IT-решений и обеспечивает определенные преимущества для команды разработки. Суммируем основные особенности GraphQL, с которыми мы встретились при разработке проекта:
- Генерация типов. Генерация типов graphql позволяет экономить время при написании запросов, идеально подходит для использования TypeScript и помогает отлавливать неприятные ошибки на раннем этапе разработки.
- Быстрота обновления данных. С GraphQL в нашем случае можно было быстро обновить данные в фронтэнд-части приложения. У разработчиков была возможность вносить изменения на стороне клиента, не вмешиваясь в работу сервера (при наличии необходимых полей на бекэнде, конечно). При этом у graphql есть так называемая «песочница» для запросов, в которой можно протестировать запросы с разными параметрами
- Обработка только нужных данных. В graphql-запросах можно точно указать, какие данные следует получать. При отправке большого количества запросов это позволяет избежать обработки излишних данных.
- Масштабируемость. GraphQL делает удобнее работу с масштабируемыми системами, поскольку облегчает объединение данных из нескольких сервисов в один.
Спасибо за внимание! Надеемся, что этот пример был вам полезен.
ссылка на оригинал статьи https://habr.com/ru/company/simbirsoft/blog/518710/